/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package br.gov.cemaden.library.dao;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.Transaction;

/**
 *
 * @author paulo.dias
 */
public class GenericDao {    

    /**
     * Salva ou Atualiza dependendo se já existe ou não o objeto no banco de
     * dados.
     *
     * @param em
     * @param o: Entidade a ser Salva (se ainda não existir) ou a ser Atualizada
     * (se já existir)
     * @return Retorna a Entidade passada como parâmetro.
     * @throws Exception
     */
    public Object saveOrUpdate(Session em, Object o) throws Exception {
        
        Transaction trans = em.beginTransaction();
        try {
            trans.begin();
            em.saveOrUpdate(o);
        } catch (HibernateException e) {
            if (trans.isActive()) {
                trans.rollback();
            }
            e.printStackTrace();
            throw e;
        } finally {
            trans.commit();
            return o;
            //em.close();
        }
    }

    /**
     * Salva ou Atualiza dependendo se já existe ou não o objeto no banco de
     * dados.
     *
     * @param em
     * @param list: lista das Entidades a serem Salvas (se ainda não existir) ou
     * a serem Atualizadas (se já existir)
     * @throws Exception
     */
    public void saveOrUpdateAll(Session em, ArrayList<Object> list) throws Exception {
        
        Transaction trans = em.beginTransaction();
        try {
            trans.begin();
            for (int i = 0; i < list.size(); i++) {
                Object o = new Object();
                o = list.get(i);
                em.merge(o);
                //Isso permite fazer insertInBacth(Inserção em Lote, super rápido)
                if (i % 10 == 0) {
                    em.flush();
                    em.clear();
                }
            }
        } catch (Exception e) {
            if (trans.isActive()) {
                trans.rollback();
            }
            e.printStackTrace();
            throw e;
        } finally {
            trans.commit();
            //em.close();
        }
    }

    /**
     * Remove a Entidade do banco de dados.
     *
     * @param em
     * @param o: Entidade a ser removido do banco de dados.
     * @throws Exception
     */
    public void remove(Session em, Object o) throws Exception {

        Transaction trans = em.beginTransaction();
        try {
            trans.begin();
            em.delete(o);
        } catch (HibernateException e) {
            if (trans.isActive()) {
                trans.rollback();
            }
            throw e;
        } finally {
            trans.commit();
            //em.close();
        }
    }    

    /**
     * Remove a Entidade do banco de dados.
     *
     * @param em
     * @param list: Lista de Entidades a serem removidas do banco de dados.
     * @throws Exception
     */
    public void removeAll(Session em, ArrayList<Object> list) throws Exception {
        
        Transaction trans = em.beginTransaction();
        try {
            trans.begin();
            for (int i = 0; i < list.size(); i++) {
                Object o = list.get(i);
                em.delete(o);
                //Isso permite fazer removeInBacth(Remoção em Lote, super rápido)
                if (i % 10 == 0) {
                    em.flush();
                    em.clear();
                }
            }
        } catch (HibernateException e) {
            if (trans.isActive()) {
                trans.rollback();
            }
            throw e;
        } finally {
            trans.commit();
            //em.close();
        }
    }

    /**
     * Retorna a lista de itens salvo no banco de dados de acordo com a Entidade
     * passada como parâmetro.
     *
     * @param em
     * @param classe : Entidade que deverá buscar no banco de dados.
     * @return List contendo todos os objetos encontrados da entidade passada
     * como parâmetro.
     */
    @SuppressWarnings("unchecked")
    public static List findAll(Session em, Class classe) {
        Query query = em.createQuery("Select a from " + classe.getName() + " a");
        return query.list();
    }

    /**
     * Faz a busca no banco pelo ID da entidade
     *
     * @param em
     * @param classe : Entidade a ser buscada
     * @param id : valor a ser procurado
     * @return Objeto encontrado no banco, caso não encontre será retornado NULL
     * @throws Exception
     */
    @SuppressWarnings("unchecked")
    public static Object findById(Session em, Class classe, Object id) throws Exception {
        if (classe == null) {
            throw new Exception("classe não pode ser nula.");
        }
        if (id == null) {
            throw new Exception("id não pode ser nulo.");
        }
        Field[] fields = classe.getDeclaredFields();
        String chave = "";
        for (Field field : fields) {
            if (field.getAnnotation(Id.class) != null) {
                chave = field.getName();
                break;
            }
        }
        if (chave.equals("")) {
            throw new Exception("Annotation @Id não encontrada na classe " + classe.getName());
        }
        
        Query query = em.createQuery("Select a from " + classe.getName() + " a"
                + " where a." + chave + " = :id").setParameter("id", id);
        query.setMaxResults(1);
        return query.uniqueResult();
    }

    /**
     * Retorna objeto de acordo com a UniqueConstraint definida na Entidade
     *
     * @param em
     * @param classe : Entidade a ser buscada
     * @param ids : Array contendo os IDS a serem buscados (na mesma ordem em
     * que a UniqueConstraint foi definido)
     * @return Objeto encontrado no banco, caso não encontre será retornado NULL
     * @throws Exception
     */
    @SuppressWarnings("unchecked")
    public static Object findByUniqueConstraints(Session em, Class classe, ArrayList<Object> ids) throws Exception {
        if (classe == null) {
            throw new Exception("classe não pode ser nula.");
        }

        if (ids == null || ids.size() == 0) {
            throw new Exception("ids não pode ser nulo.");
        }

        ArrayList<String> chaves = new ArrayList<String>();
        ArrayList<Class> tipos = new ArrayList<Class>();
        Annotation annotation = classe.getAnnotation(Table.class);
        if (annotation != null) {
            Table table = (Table) annotation;

            for (UniqueConstraint uniqueConstraint : table.uniqueConstraints()) {
                for (String column : uniqueConstraint.columnNames()) {
                    tipos.add(classe.getDeclaredField(column).getType());
                    chaves.add(column);
                }
            }
        } else {
            throw new Exception("Annotation @Table não encontrada na classe " + classe.getName());
        }

        if (chaves.size() == 0) {
            throw new Exception("UniqueConstraints não encontrada na annotation @Table da classe " + classe.getName());
        }

        if (chaves.size() != ids.size()) {
            throw new Exception("O número de parâmetros passados não confere com o número de UniqueConstraints encontrados na classe " + classe.getName() + ". " + ids.size() + " foram passados, mas " + chaves.size() + " eram esparados.");
        }

        String str = "";
        for (int i = 0; i < chaves.size(); i++) {
            if (!ids.get(i).getClass().equals(tipos.get(i))) {
                throw new Exception("Tipos de dados incompatíveis. O parâmetro número " + i + " é incompatível, foi passado:" + ids.get(i).getClass().toString() + " e estava esperando: " + tipos.get(i).toString());
            }
            if (str.length() == 0) {
                str = " where a." + chaves.get(i) + " = " + ids.get(i);
            } else {
                str += " and " + chaves.get(i) + " = " + ids.get(i);
            }
        }

        Query query = em.createQuery("Select a from " + classe.getName() + " a " + str);
        query.setMaxResults(1);
        return query.uniqueResult();
    }
}
